namespace Legend.IoCo.Tests
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using FakeItEasy;
    using FakeItEasy.Expressions;
    using NUnit.Framework;

    [TestFixture]
    public class ContainerTests
    {
        private static Func<IComponentResolver, object> AnyCreator
        {
            get
            {
                return A<Func<IComponentResolver, object>>.Ignored;
            }
        }

        private IContainer container;

        [SetUp]
        public void SetUp()
        {
            this.container = A.Fake<IContainer>();
        }

        [Test]
        public void Register_should_call_register_on_container_with_specified_type()
        {
            // Arrange
            Func<IComponentResolver, IFoo> creator = s => A.Fake<IFoo>();

            // Act
            this.container.RegisterAs<IFoo>(creator);

            // Assert
            A.CallTo(() => this.container.Register(A<IEnumerable<Type>>.That.ContainsType(typeof(IFoo)), AnyCreator)).MustHaveHappened();
        }

        [Test]
        public void Register_should_call_register_with_function_that_invokes_passed_in_creator()
        {
            // Arrange
            var returnedComponent = A.Fake<IFoo>();
            Func<IComponentResolver, IFoo> creator = s => returnedComponent;

            // Act
            this.container.RegisterAs<IFoo>(creator);

            // Assert
            A.CallTo(() => this.container.Register(A<IEnumerable<Type>>._,  A<Func<IComponentResolver, object>>.That.Returns(null, returnedComponent))).MustHaveHappened();
        }

        [Test]
        public void RegisterAs_with_extra_types_should_call_register_on_container_with_all_types()
        {
            // Arrange
            Func<IComponentResolver, IFoo> creator = s => A.Fake<IFoo>();

            // Act
            this.container.RegisterAs<IFoo>(new Type[] { typeof(object) }, creator);

            // Assert
            A.CallTo(() => this.container.Register(A<IEnumerable<Type>>.That.ContainsType(typeof(IFoo)), AnyCreator)).MustHaveHappened();
            A.CallTo(() => this.container.Register(A<IEnumerable<Type>>.That.ContainsType(typeof(object)), AnyCreator)).MustHaveHappened();
        }

        [Test]
        public void RegisterAs_with_extra_types_should_call_register_with_function_that_invokes_passed_in_creator()
        {
            // Arrange
            var returnedComponent = A.Fake<IFoo>();
            Func<IComponentResolver, IFoo> creator = s => returnedComponent;

            // Act
            this.container.RegisterAs<IFoo>(new Type[] { typeof(object) }, creator);

            // Assert
            A.CallTo(() => this.container.Register(A<IEnumerable<Type>>._, A<Func<IComponentResolver, object>>.That.Returns(null, returnedComponent))).MustHaveHappened();
        }

        [Test]
        public void Resolve_should_return_resolved_instance_when_resolving_is_successful()
        {
            // Arrange
            object outParameter = null;
            var returnedFoo = A.Fake<IFoo>();
            A.CallTo(() => this.container.TryResolve(typeof(IFoo), out outParameter)).Returns(true).AssignsOutAndRefParameters(returnedFoo);

            // Act
            var result = this.container.Resolve(typeof(IFoo));
            
            // Assert
            Assert.That(result, Is.SameAs(returnedFoo));
        }

        [Test]
        public void Resolve_should_throw_exception_when_try_resolve_returns_false()
        {
            // Arrange
            object outParameter = null;
            A.CallTo(() => this.container.TryResolve(typeof(IFoo), out outParameter)).Returns(false);

            // Act, Assert
            var ex = Assert.Throws<ComponentResolveException>(() => 
                this.container.Resolve(typeof(IFoo)));

            Assert.That(ex.Message, Text.Contains("Legend.IoCo.Tests.IFoo"));
        }

        [Test]
        public void Generic_Resolve_should_return_resolved_instance_when_resolving_is_successful()
        {
            // Arrange
            object outParameter = null;
            var returnedFoo = A.Fake<IFoo>();
            A.CallTo(() => this.container.TryResolve(typeof(IFoo), out outParameter)).Returns(true).AssignsOutAndRefParameters(returnedFoo);

            // Act
            var result = this.container.Resolve<IFoo>();

            // Assert
            Assert.That(result, Is.SameAs(returnedFoo));
        }

        [Test]
        public void Generic_Resolve_should_throw_exception_when_try_resolve_returns_false()
        {
            // Arrange
            object outParameter = null;
            A.CallTo(() => this.container.TryResolve(typeof(IFoo), out outParameter)).Returns(false);

            // Act, Assert
            Assert.Throws<ComponentResolveException>(() =>
                this.container.Resolve<IFoo>());
        }
    }

    [TestFixture]
    public class AsTests
    {
        [Test]
        public void Singleton_should_return_function_that_passes_component_resolver_to_the_original_function()
        {
            // Arrange
            IComponentResolver resolverPassedToOriginal = null;
            Func<IComponentResolver, object> original = x => 
                {
                    resolverPassedToOriginal = x;
                    return null;
                };

            var passedResolver = A.Fake<IComponentResolver>();

            // Act
            As.Singleton(original).Invoke(passedResolver);

            // Assert
            Assert.That(resolverPassedToOriginal, Is.EqualTo(passedResolver));
        }

        [Test]
        public void Singleton_should_return_function_that_produces_the_same_instance_each_time_its_called()
        {
            int i = 0;
            var function = As.Singleton(x => i++);

            Assert.That(function.Invoke(null), Is.EqualTo(0));
            Assert.That(function.Invoke(null), Is.EqualTo(0));
        }
    }

    public static class CustomArgumentConstraints
    {
        public static IEnumerable<Type> ContainsType(this IArgumentConstraintManager<IEnumerable<Type>> scope, Type type)
        {
            return scope.NullCheckedMatches(
                x => x.Any(v => v.Equals(type)),
                x => x.Write("contains type ").Write(type));
        }

        public static Func<TReturn> Returns<TReturn>(this IArgumentConstraintManager<Func<TReturn>> scope, TReturn expectedValue)
        {
            return scope.Matches(
                x =>
                {
                    return expectedValue.Equals(x.Invoke());
                },
               "A function that returns " + expectedValue);
        }

        public static Func<T, TReturn> Returns<T, TReturn>(this IArgumentConstraintManager<Func<T, TReturn>> scope, T argument, TReturn expectedValue)
        {
            return scope.Matches(
                x => 
                    {
                        return expectedValue.Equals(x.Invoke(argument));
                    },
               "A function that returns " + expectedValue);
        }
    }
}
